<?php
/**
 * model.class
 *
 * @version:	0.1a
 * @package:	global
 * @author:		alex_ez
 * @created:	28.01.2009 02:59:55
 *
 * @copyright:	(c) 2009 alex <qfox@ya.ru> yaroshevich aka alex_ez. All rights reserved.
 */

class ModelException extends Exception
{
	
}

abstract class basic_model
{
	/**
	 * таблица
	 *
	 */
	protected $table = null;

	/**
	 * таблица связей
	 * deprecated
	 */
	protected $linkedTable = null;

	/**
	 * поле, по которому таблица связей линкуется с id в основной
	 * deprecated
	 */
	protected $linkedField = null;

	/**
	 * линк к объекту базы данных
	 *
	 */
	protected $db = null;
	
	/**
	 * кол-во строк в выборке без учета limit
	 *
	 */
	protected $__foundRows = 0;
	
	/**
	 * fields that will apply to each query to main table except of get(id|tag)-like queries
	 *
	 */
	protected $__fixedFields = array( );
	
	/**
	 * конструктор
	 * создает базу и выполняет инициализацию. иногда удобнее, чем __construct(){ parent::__construct(); ... }
	 *
	 * @param string $table
	 */
	public function __construct( $table = null )
	{
		$this ->db = db::instance();
		
		$this ->onstart( );
		
		if( !is_null( $table ) )
			$this ->table = $table;
	}
	
	/**
	 * Start event
	 *
	 */
	protected function onstart( ) { }
	
	/**
	 * выборка одной записи по $id
	 *
	 */
	public function get( $id, $cacheTime = '6H' )
	{
		if( is_null( $this ->table ) ) throw new Exception( "internal error bqz table" );
		
		fb::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		//fb::log( .'( '.$id.', '.$cacheTime.' )', 'call' );
		
		$cache = cache::__getMe( );
		$__fga = $id = (int)($id); $__fcc = get_class( $this );
		if( $cache ->is( $__fcc, __METHOD__, $__fga, $cacheTime ) )
		{
			fb::log( 'from cache' );
			fb::groupEnd( );
			return $cache ->get( $__fcc, __METHOD__, $__fga );
		}
		
		//fb::log( 'recache data' );
		
		$_r = $this ->db ->_row( sprintf( "SELECT * FROM %s WHERE id = %d;", $this ->table, $id ) );
		if( is_null( $_r ) ) return null;
		
		if( $this ->isLinked( ) )
		{
			$_r['linked'] = array( );
			$_rp = $this ->db ->_list( sprintf( "SELECT * FROM %s WHERE %s = %d;", $this ->linkedTable, $this ->linkedField, $id ), 'id' );
			foreach( $_rp as $k => $v )
			{
				unset($v['id']); unset($v[$this ->linkedField]);
				$_r['linked'][$k] = $v;
			}
		}
		
		$cache ->set( $__fcc, __METHOD__, $__fga, $_r );
		
		fb::groupEnd( );
		
		return $_r;
	}
	
	/**
	 * выборка одной записи через поле tag. удобно для страниц, статей, и т.д.
	 *
	 */
	public function getByTag( $tag, $cacheTime = '3H' )
	{
		fb::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		if( !is_string( $tag ) ) throw new Exception( "wrong parameter in ".__METHOD__ );
		if( is_null( $this ->table ) ) throw new Exception( "internal error bqz table in ".__METHOD__ );
		
		$cache = cache::__getMe( );
		$__fga = $tag; $__fcc = get_class( $this );
		if( $cache ->is( $__fcc, __METHOD__, $__fga, $cacheTime ) )
		{
			fb::groupEnd( );
			return $cache ->get( $__fcc, __METHOD__, $__fga );
		}
		
		$_r = $this ->db ->_row( sprintf( "SELECT * FROM %s WHERE tag = '%s';", $this ->table, $this ->db ->escape_string( $tag ) ) );
		$cache ->set( $__fcc, __METHOD__, $__fga, $_r );
		
		fb::groupEnd( );
		
		return $_r;
	}
	
	/**
	 * getBy
	 * выборка списка по заданным параметрам.
	 *
	 * @param	array $p	array( 'id' => array( 1,2,5,14 ), ... ) или array( 'tag' => $tag ). собираются через AND
	 * @param	string $o	сортировка. "id+", "id desc", "item_type+,posted-", "item_type asc, posted desc"
	 * @param	array $l	лимит. массив из двух значений. array( 0, 100 ) => limit 0, 100
	 */
	public function getBy( $p = null, $o = null, $l = null, $cacheTime = '12H' )
	{
		$this ->checkupSql( $p, $o, $l );
		
		if( $cacheTime ) {
		$cache = cache::__getMe( );
		$__fga = array( $p, $this ->__fixedFields, $o, $l ); $__fcc = get_class( $this );
		if( $cache ->is( $__fcc, __METHOD__, $__fga, $cacheTime ) )
		{
			return $cache ->getv( );
		}
		}
		
		fb::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		$sql = $this ->makeSql( $this ->table, '*', $p, $o, $l );
		
		$_r = $this ->db ->_list( $sql, 'id' );
		$_r = array_filter( $_r );
		$this ->__foundRows = $this ->db ->_value( "SELECT FOUND_ROWS();" );
		
		if( empty( $_r ) ) {
			fb::log( 'empty result' );
			fb::groupEnd( );
			return $_r;
		}
		
		// linked
		if( $this ->isLinked( ) && !empty( $_r ) )
		{
			$ids = array_map( 'intval', array_keys( $_r ) );
			foreach( $ids as $k ) if(isset($_r[$k])) $_r[$k]['linked'] = array( );
			$_rp = $this ->db ->_list( sprintf( "SELECT * FROM %s WHERE %s in ( %s );", $this ->linkedTable, $this ->linkedField, join( ', ', $ids ) ) );
			foreach( $_rp as $k => $v )
			{
				$item_id = $v[ $this ->linkedField ];
				unset($v['id']); unset( $v[ $this ->linkedField ] );
				$_r[$item_id]['linked'][$k] = $v;
			}
		}
		
		if( $cacheTime ) {
		$cache ->setv( $_r );
		}

		fb::groupEnd( );
		
		return $_r;
	}

	protected function checkupSql( &$p = null, &$o = null, &$l = null )
	{
		if( is_null( $this ->table ) ) throw new Exception( "internal error bqz table" );
		if( !is_array( $p ) && ( !is_string( $p ) ) ) throw new Exception( "wrong where in ".__METHOD__ );
		if( is_string( $o ) && !strlen( $o ) ) $o = null;
		if( !is_null( $o ) && ( !is_string( $o ) || !strlen( $o ) ) ) throw new Exception( "wrong order in ".__METHOD__ );
		if( !is_null( $l ) && ( !is_array( $l ) || empty( $l ) ) ) { if( is_numeric( $l ) && intval( $l ) ) $l = array( 0, intval( $l ) ); else throw new Exception( "wrong limit in ".__METHOD__ ); }
	}
	
	protected function makeSql( $table, $columns = null, $p = null, $o = null, $l = null )
	{
		// columns
		if( is_null( $columns ) )
			$columns = '*';
		elseif( is_array( $columns ) )
			$columns = '`'.join( '`,`', $columns ).'`';
		
		// where
		if( is_array( $p ) ) {
			$p = array_merge( $p, $this ->__fixedFields );
			$_sp = $this ->db ->makeSetList( $p, 'AND' );
		} else {
			$_sp = $this ->db ->makeSetList( $this ->__fixedFields, 'AND' );
			//fb::log( $p, 'in' );
			$p = join( ' AND ', array_filter( array_map( 'trim', explode( 'AND', $p ) ) ) );
			//fb::log( $p, 'modified' );
			$_sp .= ( $_sp ) ? ' AND ( '.$p.' )' : $p;
		}
		if( !$_sp ) $_sp = 'TRUE';
		$_sp = ' '.$_sp.' ';

		// order
		$order = '';
		if( !is_null( $o ) && is_string( $o ) )
		{
			if( strpos( $o, 'ASC' )||strpos( $o, 'DESC' ) )
			{
				$order = 'ORDER BY '.$o;
			}
			else
			{
				$o = explode( ',', $o );
				$o = array_map( create_function( '$a', '$a=trim($a); if(mb_strtolower(mb_substr($a,-4,4))==" asc") { return mb_substr($a,0,-4)."+"; } elseif(mb_strtolower(mb_substr($a,-5,5))==" desc") return mb_substr($a,0,-5)."-"; return $a;' ), $o );
				$o = array_map( create_function( '$a', '$a=trim($a); if(strpos($a,"`")!==false) return $a; $o=mb_substr($a,-1,1); if($o!="-")$o="+"; return sprintf("`%s`%s",trim($a," ".$o),$o);' ), $o );
				fb::log( $o, 'order' );
				$order = 'ORDER BY '.trim(preg_replace( array( '/\\+(,|$)/', '/\\-(,|$)/' ), array( ' ASC, ', ' DESC, ' ), join( ',', $o ) ),' ,');
			}
		}

		// limit
		$directive = "";
		$limit = "";
		if( !is_null( $l ) )
		{
			$limit = ' LIMIT '.join( ',', $l );
			$directive = 'SQL_CALC_FOUND_ROWS';
		}
		
		$sql = sprintf( "SELECT %s %s FROM %s WHERE %s %s %s;", $directive, $columns, $table, $_sp, $order, $limit );
		return $sql;
	}
	
	public function getFoundRows( )
	{
		return $this ->__foundRows;
	}
	
	public function getCount( )
	{
		$cache = cache::__getMe( ); $__fga = null; $__fcc = get_class( $this );
		if( $cache ->is( $__fcc, __METHOD__, $__fga, '20M' ) )
			return $cache ->get( $__fcc, __METHOD__, $__fga );
		
		$_r = $this ->db ->_value( "SELECT COUNT(id) as `count` FROM {$this->table};" );
		$cache ->set( $__fcc, __METHOD__, $__fga, $_r );
		
		return $_r;
	}
	
	public function count( $where = null )
	{
		return is_null( $where )
			? $this ->getCount( )
			: count( $this ->getBy( $where ) );
	}
	
	
	public function add( $data, $ignore = false )
	{
		fb::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		$data = array_merge( $data, $this ->__fixedFields );
		$datas = $this ->db ->makeSetList( $data );
		
		$operator = $ignore?($ignore==1?"INSERT IGNORE":"REPLACE"):"INSERT";
		
		$sql = "%s INTO %s SET %s;";
		$sql = sprintf( $sql, $operator, $this ->table, $datas );
		
		$this ->db ->exec( $sql );
		$this ->__foundRows = $this ->db ->affected_rows;
		//fb::log( array( 'ar' => $this ->db ->affected_rows, 'id' => $this ->db ->insert_id ), 'add'.$ignore );
		
		$r = $ignore == 2
			? ( $this ->db ->affected_rows != 0 ? $this ->db ->insert_id : false )
			: ( $this ->db ->affected_rows == 1 ? $this ->db ->insert_id : false );
		
		fb::log( $r, 'result' );
		fb::groupEnd( );
		return $r;
	}
	
	public function remove( $id )
	{
		fb::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		$ids = array_merge( array( 'id' => intval( $id ) ), $this ->__fixedFields );
		$ids = $this ->db ->makeSetList( $ids, 'AND' );
		
		$sql = "DELETE FROM `%s` WHERE %s;";
		$sql = sprintf( $sql, $this ->table, $ids );
		$this ->db ->exec( $sql );
		$this ->__foundRows = $this ->db ->affected_rows;
		
		$r = $this ->db ->affected_rows;
		fb::log( $r, 'result' );
		fb::groupEnd( );
		return $r;
	}
	
	public function edit( $id, $data )
	{
		fb::group( get_class($this).'::'.__METHOD__/*, array('Collapsed'=>true,'Color'=>'#159')*/ );
		
		$datas = $this ->db ->makeSetList( $data );
		
		$ids = array_merge( array( 'id' => intval( $id ) ), $this ->__fixedFields );
		$ids = $this ->db ->makeSetList( $ids, ' AND ' );
		
		$sql = "UPDATE `%s` SET %s WHERE %s;";
		$sql = sprintf( $sql, $this ->table, $datas, $ids );
		$this ->db ->exec( $sql );
		$this ->__foundRows = $this ->db ->affected_rows;
		
		$r = $this ->db ->affected_rows;
		fb::log( $r, 'result' );
		fb::groupEnd( );
		return $r;
	}
	
	
	
	
	// linked
	public function isLinked( $exceptionOnFail = false )
	{
		if( !$this ->linkedOn
		 || is_null( $this ->linkedTable ) || !isset( $this ->linkedTable[1] )
		 || is_null( $this ->linkedField ) || !isset( $this ->linkedField[1] ) )
		{
			if( !$exceptionOnFail ) return false;
			throw new Exception( sprintf( texo::translate('Model "%s" have no linked table.field pair'), get_class( $this ) ) );
		}
		
		return true;
	}
	
	protected $linkedOn = true;
	public function dontLoadLinked( )
	{
		$this ->linkedOn = false;
	}
	public function doLoadLinked( )
	{
		$this ->linkedOn = true;
	}

	
	public function addLink( $data, $id = null )
	{
		$this ->isLinked( 1 );
		
		if( !is_null( $id ) ) // add id to data
			$data[ $this ->linkedField ] = $id;
		
		$data = $this ->db ->makeSetList( $data );
		
		$sql = "REPLACE INTO %s SET %s;";
		$sql = sprintf( $sql, $this ->linkedTable, $data );
		
		$this ->db ->exec( $sql );
		
		return ( $this ->db ->affected_rows != 0 ? $this ->db ->insert_id : false );
	}
	
	// todo: rewrite
	public function addLinks( $datas, $id = null )
	{
		$this ->isLinked( 1 );
		
		if( !isset( $this ->linkedTable ) || !isset( $this ->linkedTable[1] ) ) return null;
		$_r = array( );
		foreach( $datas as $k => $data )
		{
			if( !is_null( $id ) ) // add item_id to each data
				$data[ $this ->linkedField ] = $id;
			$_r[$k] = $this ->addLink( $data );
		}
		
		return $_r;
	}
	
	public function removeLink( $ids )
	{
		$this ->isLinked( 1 );
		
		$ids = $this ->db ->makeSetList( $ids, 'AND' );
		
		$sql = "DELETE FROM `%s` WHERE %s;";
		$sql = sprintf( $sql, $this ->linkedTable, $ids );
		$this ->db ->exec( $sql );
		
		return $this ->db ->affected_rows;
	}
	
	public function removeLinks( $linked_id )
	{
		$this ->isLinked( 1 );
		
		return $this ->removeLinksByKeys( array( $this ->linkedField => $linked_id ) );
	}
	
/*
	public function removeLinksToItem( $item_type, $item_id )
	{
		if( !isset( $this ->linkedTable ) || !isset( $this ->linkedTable[1] ) ) return null;
		return $this ->removeLinksByKeys( array( 'item_type' => $item_type, 'item_id' => $item_id ) );
	}
*/
	
	public function removeLinksByKeys( $keys )
	{
		$this ->isLinked( 1 );
		
		$keys = $this ->db ->makeSetList( $keys, 'AND' );
		
		fb::log($keys);
		
		$sql = "DELETE FROM %s WHERE %s;";
		$sql = sprintf( $sql, $this ->linkedTable, $keys );
		$this ->db ->exec( $sql );
		return $this ->db ->affected_rows;
	}
	
	
	
	/**
	 * States
	 */
	public function setState( $id, $state )
	{
		$this ->edit( $id, array( 'state' => $state ) );
	}
	
	public function getState( $id )
	{
		$l = $this ->get( $id );
		return $l['state'];
	}

	/**
	 * Service
	 */

	/**
	 * Gets model name
	 * @return string
	 */
	public function getName( )
	{
		return str_replace( '_', '-', get_class( $this ) );
	}
}

